/*
 * Copyright (C) 2010 Fores Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package jp.fores.midori.core.util;

import java.lang.reflect.Array;
import java.lang.reflect.Type;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import net.arnx.jsonic.JSON;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * クラス関連の処理を行うユーティリティクラス。<br>
 * (このクラスの全てのメソッドはスレッドセーフです。)<br>
 */
public class ClassUtil {
	//==========================================================
	//定数

	/**
	 * ログ出力用
	 */
	private static final Log log = LogFactory.getLog(ClassUtil.class);


	//==========================================================
	//フィールド

	/**
	 * 一度発見したクラスオブジェクトをキャッシュするためのMap
	 */
	private static final ConcurrentMap<String, Class<?>> classCacheMap = new ConcurrentHashMap<String, Class<?>>();


	//==========================================================
	//メソッド

	//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
	//コンストラクタ

	/**
	 * コンストラクタです。<br>
	 * (クラスメソッドのみのユーティリティークラスなので、privateにしてインスタンスを作成できないようにしています。)<br>
	 */
	private ClassUtil() {
		//基底クラスのコンストラクタの呼び出し
		super();
	}


	//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
	//クラスメソッド

	/**
	 * 標準のClass.forName()メソッドを改良して、クラスローダーの優先度を考慮するようにしたメソッドです。<br>
	 * 最初に現在のスレッドのクラスローダーからの取得を試みます。<br>
	 * 失敗した場合は、このクラスが属するクラスローダーから取得を試みます。<br>
	 * それでも失敗した場合は、標準のClass.forName()メソッドを呼び出します。<br>
	 *
	 * @param name 要求するクラスの完全指定の名前
	 * @return 要求するクラスを表すクラスオブジェクト
	 * @throws ClassNotFoundException クラスが見つからなかった場合
	 */
	public static Class<?> forName(String name) throws ClassNotFoundException {
		try {
			//現在のスレッドのクラスローダーからの取得を試みる
			return Class.forName(name, true, Thread.currentThread().getContextClassLoader());
		}
		//失敗した場合
		catch (ClassNotFoundException e) {
			try {
				//このクラスが属するクラスローダーから取得を試みる
				return Class.forName(name, true, ClassUtil.class.getClassLoader());
			}
			//それでも失敗した場合
			catch (ClassNotFoundException e2) {
				//標準のClass.forName()メソッドを呼び出す
				return Class.forName(name);
			}
		}
	}

	/**
	 * {@link #forName(String)}メソッドのキャッシュをとるバージョンです。<br>
	 * 一度取得に成功したクラスオブジェクトをメモリ上にキャッシュすることにより、以降は高速に取得することができます。<br>
	 *
	 * @param name 要求するクラスの完全指定の名前
	 * @return 要求するクラスを表すクラスオブジェクト
	 * @throws ClassNotFoundException クラスが見つからなかった場合
	 */
	public static Class<?> forNameWithCache(String name)
			throws ClassNotFoundException {
		//一度発見したクラスオブジェクトをキャッシュするためのMapから取得を試みる
		Class<?> clazz = classCacheMap.get(name);

		//キャッシュから取得できなかった場合
		if (clazz == null) {
			//クラス名に対応するクラスオブジェクトを取得する
			clazz = ClassUtil.forName(name);

			//取得したクラスオブジェクトをキャッシュするためのMapにアトミックに登録する
			Class<?> originalClass = classCacheMap.putIfAbsent(name, clazz);

			//Mapへの登録時の戻り値がnullでない場合
			//(すでに別の値が登録されていた場合)
			if (originalClass != null) {
				//すでに登録されていた値を採用する
				clazz = originalClass;
			}
		}

		//取得したクラスオブジェクトを返す
		return clazz;
	}

	/**
	 * オブジェクトの型補正を行います。<br>
	 *
	 * @param <T> 補正する型
	 * @param obj 型補正を行うオブジェクト
	 * @param clazz 補正する型のクラスオブジェクト
	 * @return 補正後のオブジェクト
	 * @throws IllegalArgumentException 型補正ができなかった場合
	 */
	public static <T> T adjustObjectType(Object obj, Class<T> clazz)
			throws IllegalArgumentException {
		//オブジェクトがnullの場合
		if (obj == null) {
			//nullを返す
			return null;
		}

		//オブジェクトのクラスオブジェクトを取得
		Class objClass = obj.getClass();

		//オブジェクトと補正する型のどちらも配列の場合
		if (objClass.isArray() && clazz.isArray()) {
			//先に配列自体の型補正を行う
			//(中身のオブジェクトの型が正しくても、配列自体の型が異なる場合に対応するため)
			//(具体的には、オブジェクトがObjectの配列で、補正する型がDTOの配列の場合などに対応するため)
			obj = adjustArray(obj, clazz);
		}
		//オブジェクトが補正する型と互換性がある場合
		else if (objClass.isAssignableFrom(clazz)) {
			//普通にキャストして返す
			return (T) obj;
		}

		try {
			//JSONICのJSONクラスに型補正を委譲して、結果をキャストして返す
			//(TODO:できればJSONICに依存しないようにしたいが、かなり難しいので今は妥協する)
			return (T) new JSON().convert(obj, clazz);
		}
		//例外処理
		catch (Exception e) {
			//デバッグログが有効な場合
			if (log.isDebugEnabled()) {
				//デバッグログ出力
				log.debug(obj.toString() + "を" + clazz.getName()
						+ "に型補正できませんでした。", e);
			}

			//補足した例外をそのまま投げたくないので、IllegalArgumentExceptionを新規に作成して投げる
			throw new IllegalArgumentException(obj.toString() + "を"
					+ clazz.getName() + "に型補正できませんでした。");
		}
	}

	/**
	 * オブジェクトの型補正を行います。<br>
	 *
	 * @param <T> 補正する型
	 * @param obj 型補正を行うオブジェクト
	 * @param clazz 補正する型のクラスオブジェクト
	 * @param type 補正する型のTypeオブジェクト
	 * @return 補正後のオブジェクト
	 * @throws IllegalArgumentException 型補正ができなかった場合
	 */
	public static <T> T adjustObjectType(Object obj, Class<T> clazz, Type type)
			throws IllegalArgumentException {
		//オブジェクトがnullの場合
		if (obj == null) {
			//nullを返す
			return null;
		}

		//オブジェクトのクラスオブジェクトを取得
		Class objClass = obj.getClass();

		//オブジェクトと補正する型のどちらも配列の場合
		if (objClass.isArray() && clazz.isArray()) {
			//先に配列自体の型補正を行う
			//(中身のオブジェクトの型が正しくても、配列自体の型が異なる場合に対応するため)
			//(具体的には、オブジェクトがObjectの配列で、補正する型がDTOの配列の場合などに対応するため)
			obj = adjustArray(obj, clazz);
		}
		//オブジェクトが補正する型と互換性がある場合
		else if (objClass.isAssignableFrom(clazz)) {
			//普通にキャストして返す
			return (T) obj;
		}

		try {
			//JSONICのJSONクラスに型補正を委譲して、結果をキャストして返す
			//(TODO:できればJSONICに依存しないようにしたいが、かなり難しいので今は妥協する)
			return (T) new JSON().convert(obj, type);
		}
		//例外処理
		catch (Exception e) {
			//補足した例外をそのまま投げたくないので、IllegalArgumentExceptionを新規に作成して投げる
			throw new IllegalArgumentException(obj.toString() + "を"
					+ clazz.getName() + "に型補正できませんでした。");
		}
	}

	/**
	 * Typeオブジェクトを見やすい形式の文字列に変換します。<br>
	 * このメソッドはFieldクラスのパッケージスコープメソッドであるgetTypeName()を真似たものです。<br>
	 *
	 * @param type Typeオブジェクト
	 * @return 見やすい形式の文字列
	 */
	public static String typeToString(Type type) {
		//引数のTypeオブジェクトがClassクラスのインスタンスでない場合
		if (!(type instanceof Class)) {
			//引数のTypeオブジェクトの文字列表現をそのまま返す
			return type.toString();
		}

		//引数のTypeオブジェクトがClassに変換
		Class clazz = (Class) type;

		//配列の場合
		if (clazz.isArray()) {
			//配列の次元
			int dimension = 0;

			//クラスオブジェクトが配列の間ループをまわす
			while (clazz.isArray()) {
				//配列の次元を+1する
				dimension++;

				//配列のコンポーネント型を表す Classオブジェクトに差し替える
				//(配列を上にたどっていく)
				clazz = clazz.getComponentType();
			}

			//作業用StringBuilderのインスタンスを生成
			StringBuilder sb = new StringBuilder();

			//一番上までたどったクラス名を作業用StringBuilderに追加
			sb.append(clazz.getName());

			//配列の次元の回数だけループをまわす
			for (int i = 0; i < dimension; i++) {
				//「[]」を作業用StringBuilderに追加
				sb.append("[]");
			}

			//作業用StringBuilderを文字列に変換して返す
			return sb.toString();
		}
		//それ以外の場合
		else {
			//クラス名だけを返す
			return clazz.getName();
		}
	}


	//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
	//内部処理用

	/**
	 * 配列の型補正を行います。<br>
	 *
	 * @param obj 型補正を行うオブジェクト(配列)
	 * @param clazz 補正する型のクラスオブジェクト(配列のクラスオブジェクト)
	 * @return 補正後のオブジェクト
	 */
	private static Object adjustArray(Object obj, Class<?> clazz) {
		//オブジェクトの配列タイプを取得
		Class objComponentType = obj.getClass().getComponentType();

		//補正する型の配列タイプを取得
		Class clazzComponentType = clazz.getComponentType();

		//オブジェクトの配列タイプと補正する型の配列タイプが同じ場合
		//(補正が必要ない場合)
		if (objComponentType.equals(clazzComponentType)) {
			//元のオブジェクトを返す
			return obj;
		}


		//オブジェクトを配列にキャスト
		Object[] originalArray = (Object[]) obj;

		//補正する型の配列タイプにあわせた配列を生々
		Object[] newArray = (Object[]) Array.newInstance(clazzComponentType, originalArray.length);

		//元のオブジェクトの配列の内容を新しい配列にコピーする
		System.arraycopy(originalArray, 0, newArray, 0, originalArray.length);

		//新しい配列を返す
		return newArray;
	}

}
